#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""\
Convert SVG to TikZ/PGF commands for use with (La)TeX

This script is an Inkscape extension for exporting from SVG to (La)TeX. The
extension recreates the SVG drawing using TikZ/PGF commands, a high quality TeX
macro package for creating graphics programmatically.

The script is tailored to Inkscape SVG, but can also be used to convert arbitrary
SVG files from the command line. 

Author: Kjell Magne Fauske
"""

# Copyright (C) 2008, 2009, 2010 Kjell Magne Fauske, http://www.fauskes.net
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

__version__ = '0.1dev'
__author__ = 'Kjell Magne Fauske'


# Todo:
# Basic functionality:

# Stroke properties
#   - markers (map from Inkscape to TikZ arrow styles. No 1:1 mapping)
# Fill properties
#   - linear shading
#   - radial shading
# Paths:
#
# Text
# 
# Other stuff:
# - Better output code formatting!
# - Add a + prefix to coordinates to speed up pgf parsing
# - Transformations
#   - default property values.The initial fill property is set to 'black'.
#     This is currently not handled. 
# - ConTeXt template support.
# TODO: Add a testing interface
import sys
from itertools import izip
from textwrap import wrap
from copy import deepcopy
import codecs
import itertools
import string
import StringIO
import copy

try:
    # This should work when run as an Inkscape extension
    import inkex
    import simplepath
    import simplestyle
except ImportError:
    # Use bundled files when run as a module or command line tool 
    from svg2tikz.inkexlib import inkex
    from svg2tikz.inkexlib import simplepath
    from svg2tikz.inkexlib import simplestyle

import pprint, os, re,math

from math import sin, cos, atan2, ceil
import logging

try:
    set
except NameError:
    # For Python 2.4 compatability
    from sets import Set as set
    


#### Utility functions and classes

class Bunch(object):
    def __init__(self, **kwds):
        self.__dict__.update(kwds)
    def __str__(self):
        return self.__dict__.__str__()
    def __repr__(self):
        return self.__dict__.__repr__()

def copy_to_clipboard(text):
    """Copy text to the clipboard

    Returns True if successful. False otherwise.

    Works on Windows, *nix and Mac. Tries the following:
    1. Use the win32clipboard module from the win32 package.
    2. Calls the xclip command line tool (*nix)
    3. Calls the pbcopy command line tool (Mac)
    4. Try pygtk
    """
    # try windows first
    try:
        import win32clipboard
        win32clipboard.OpenClipboard()
        win32clipboard.EmptyClipboard()
        win32clipboard.SetClipboardText(text)
        win32clipboard.CloseClipboard()
        return True
    except:
        pass
    # try xclip
    try:
        import subprocess
        p = subprocess.Popen(['xclip', '-selection', 'c'], stdin=subprocess.PIPE)
        p.stdin.write(text)
        p.stdin.close()
        retcode = p.wait()
        return True
    except:
        pass
    # try pbcopy (Os X)
    try:
        import subprocess
        p = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE)
        p.stdin.write(text)
        p.stdin.close()
        retcode = p.wait()
        return True
    except:
        pass
    # try os /linux
    try:
        import subprocess
        p = subprocess.Popen(['xsel'], stdin=subprocess.PIPE)
        p.stdin.write(text)
        p.stdin.close()
        retcode = p.wait()
        return True
    except:
        pass
    # try pygtk
    try:
        # Code from
        # http://www.vector-seven.com/2007/06/27/
        #    passing-data-between-gtk-applications-with-gtkclipboard/
        import pygtk
        pygtk.require('2.0')
        import gtk
        # get the clipboard
        clipboard = gtk.clipboard_get()
        # set the clipboard text data
        clipboard.set_text(text)
        # make our data available to other applications
        clipboard.store()
        return True
    except:
        pass
    # try clip (Vista)
    try:
        import subprocess
        p = subprocess.Popen(['clip'], stdin=subprocess.PIPE)
        p.stdin.write(text)
        p.stdin.close()
        retcode = p.wait()
        return True
    except:
        pass

    return False

def nsplit(seq, n=2):
    """Split a sequence into pieces of length n

    If the length of the sequence isn't a multiple of n, the rest is discarded.
    Note that nsplit will strings into individual characters.

    Examples:
    >>> nsplit('aabbcc')
    [('a', 'a'), ('b', 'b'), ('c', 'c')]
    >>> nsplit('aabbcc',n=3)
    [('a', 'a', 'b'), ('b', 'c', 'c')]

    # Note that cc is discarded
    >>> nsplit('aabbcc',n=4)
    [('a', 'a', 'b', 'b')]
    """
    return [xy for xy in izip(*[iter(seq)]*n)]


def chunks(s, cl):
    """Split a string or sequence into pieces of length cl and return an iterator
    """
    for i in xrange(0, len(s), cl):
        yield s[i:i+cl]

# Adapted from Mark Pilgrim's Dive into Python book
# http://diveintopython.org/scripts_and_streams/index.html#kgp.openanything 
def open_anything(source):
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)
    except (IOError, OSError):     
        pass            

    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)
    except (IOError, OSError):            
        pass                              

    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source))


#### Output configuration section

TEXT_INDENT = "  "

CROP_TEMPLATE = r"""
\usepackage[active,tightpage]{preview}
\PreviewEnvironment{tikzpicture}
"""


# Templates
STANDALONE_TEMPLATE=r"""
\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{tikz}
%(cropcode)s
\begin{document}
%(colorcode)s
%(gradientcode)s
\begin{tikzpicture}[y=0.80pt,x=0.80pt,yscale=-1, inner sep=0pt, outer sep=0pt%(extraoptions)s]
%(pathcode)s
\end{tikzpicture}
\end{document}
"""

FIG_TEMPLATE = r"""
%(colorcode)s
%(gradientcode)s
\begin{tikzpicture}[y=0.80pt, x=0.8pt,yscale=-1, inner sep=0pt, outer sep=0pt%(extraoptions)s]
%(pathcode)s
\end{tikzpicture}
"""


SCALE = 'scale'
DICT = 'dict'
DIMENSION = 'dimension'
FACTOR = 'factor' # >= 1

# Map Inkscape/SVG stroke and fill properties to corresponding TikZ options.
# Format:
#   'svg_name' : ('tikz_name', value_type, data)
PROPERTIES_MAP = {
    'opacity' : ('opacity',SCALE,''),
    # filling    
    'fill-opacity' : ('fill opacity', SCALE,''),
    'fill-rule' : ('',DICT,
                   dict(nonzero='nonzero rule',evenodd='even odd rule')),
    # stroke    
    'stroke-opacity' : ('draw opacity', SCALE,''),
    'stroke-linecap' : ('line cap',DICT,
                        dict(butt='butt',round='round',square='rect')),
    'stroke-linejoin' : ('line join',DICT,
                         dict(miter='miter',round='round',bevel='bevel')),
    'stroke-width' : ('line width',DIMENSION,''),
    'stroke-miterlimit' : ('miter limit', FACTOR,''),
    'stroke-dashoffset' : ('dash phase',DIMENSION,'0')
}

# default values according to the SVG spec.
DEFAULT_PAINTING_VALUES = {
    # fill
    'fill' : 'black',
    'fill-rule' : 'nonzero',
    'fill-opacity' : 1,
    # stroke
    'stroke' : 'none',
    'stroke-width' : 1,
    'stroke-linecap' : 'butt',
    'stroke-linejoin' : 'miter',
    'stroke-miterlimit' : 4,
    'stroke-dasharray' : 'none',
    'stroke-dashoffset' : 0,
    'stroke-opacity' : 1,
}

STROKE_PROPERTIES = set([
    'stroke', 'stroke-width', 'stroke-linecap', 
    'stroke-linejoin', 'stroke-miterlimit',
    'stroke-dasharray', 'stroke-dashoffset',
    'stroke-opacity',
])

FILL_PROPERTIES = set([
    'fill', 'fill-rule', 'fill-opacity',
])


# The calc_arc function is based on the calc_arc function in the
# paths_svg2obj.py script bundled with Blender 3D
# Copyright (c) jm soler juillet/novembre 2004-april 2007, 
def calc_arc(cpx, cpy, rx, ry, ang, fa, fs, x, y) :
    """
    Calc arc paths
    """
    PI = math.pi
    ang = math.radians(ang)
    rx = abs(rx)
    ry = abs(ry)
    px = abs((cos(ang)*(cpx-x)+sin(ang)*(cpy-y))*0.5)**2.0
    py = abs((cos(ang)*(cpy-y)-sin(ang)*(cpx-x))*0.5)**2.0
    rpx = rpy = 0.0
    if abs(rx) > 0.0:
        rpx = px/(rx**2.0)
    if abs(ry) > 0.0:
        rpy = py/(ry**2.0)
    pl = rpx+rpy
    if pl>1.0:
        pl = pl**0.5
        rx*=pl;ry*=pl
    carx = sarx = cary = sary = 0.0
    if abs(rx) > 0.0:
        carx = cos(ang)/rx
        sarx = sin(ang)/rx
    if abs(ry) > 0.0:
        cary = cos(ang)/ry
        sary=sin(ang)/ry
    x0 = (carx)*cpx+(sarx)*cpy
    y0 = (-sary)*cpx+(cary)*cpy
    x1 = (carx)*x+(sarx)*y
    y1 = (-sary)*x+(cary)*y
    d = (x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)
    if abs(d) > 0.0:
        sq=1.0/d-0.25
    else:
        sq=-0.25
    if sq <0.0:
        sq=0.0
    sf = sq**0.5
    if fs == fa:
        sf=-sf
    xc = 0.5*(x0+x1)-sf*(y1-y0)
    yc = 0.5*(y0+y1)+sf*(x1-x0)
    ang_0 = atan2(y0-yc,x0-xc)
    ang_1 = atan2(y1-yc,x1-xc)
    ang_arc = ang_1-ang_0
    if (ang_arc < 0.0 and fs==1):
        ang_arc += 2.0 * PI
    elif (ang_arc > 0.0 and fs==0):
        ang_arc-=2.0*PI
    
    ang0 = math.degrees(ang_0)
    ang1 = math.degrees(ang_1)

    if ang_arc > 0:
        if (ang_0 < ang_1):
            pass
        else:
            ang0 -= 360
    else:
        if (ang_0 < ang_1):
            ang1 -= 360

    return (ang0, ang1, rx, ry)

def parse_transform(transf):
    """Parse a transformation attribute and return a list of transformations"""
    # Based on the code in parseTransform in the simpletransform.py module.
    # Copyright (C) 2006 Jean-Francois Barraud
    # Reimplemented here due to several bugs in the version shipped with
    # Inkscape 0.46
    if transf=="" or transf==None:
        return(mat)
    stransf = transf.strip()
    result = re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf)
    transforms = []
    #-- translate --
    if result.group(1) == "translate":
        args=result.group(2).replace(',',' ').split()
        dx = float(args[0])
        if len(args) == 1:
            dy = 0.0
        else:
            dy = float(args[1])
        matrix = [[1,0,dx],[0,1,dy]]
        transforms.append(['translate',(dx,dy)])
    #-- scale --
    if result.group(1)== "scale":
        args = result.group(2).replace(',',' ').split()
        sx = float(args[0])
        if len(args) == 1:
            sy = sx
        else:
            sy = float(args[1])
        matrix=[[sx,0,0],[0,sy,0]]
        transforms.append(['scale', (sx,sy)])
    #-- rotate --
    if result.group(1) == "rotate":
        args = result.group(2).replace(',', ' ').split()
        a = float(args[0])#*math.pi/180
        if len(args) == 1:
            cx, cy = (0.0,0.0)
        else:
            cx, cy = map(float, args[1:])
        matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]
        transforms.append(['rotate',(a,cx,cy)])
    #-- skewX --
    if result.group(1) == "skewX":
        a = float(result.group(2))#"*math.pi/180
        matrix = [[1,math.tan(a),0],[0,1,0]]
        transforms.append(['skewX',(a,)])
    #-- skewY --
    if result.group(1) == "skewY":
        a=float(result.group(2))#*math.pi/180
        matrix=[[1,0,0],[math.tan(a),1,0]]
        transforms.append(['skewY',(a,)])
    #-- matrix --
    if result.group(1) == "matrix":
        #a11,a21,a12,a22,v1,v2=result.group(2).replace(' ',',').split(",")
        mparams = tuple(map(float, result.group(2).replace(',',' ').split()))
        a11,a21,a12,a22,v1,v2 = mparams
        matrix=[[a11, a12, v1], [a21, a22, v2]]
        transforms.append(['matrix', mparams])

    if result.end() < len(stransf):
        return transforms + parse_transform(stransf[result.end():])
    else:
        return transforms

def parseColor(c):
    """Creates a rgb int array"""
    # Based on the code in parseColor in the simplestyle.py module
    # Fixes a few bugs. Should be removed when fixed upstreams.
    if c in simplestyle.svgcolors.keys():
        c = simplestyle.svgcolors[c]
    # need to handle 'currentColor'
    if c.startswith('#') and len(c) == 4:
        c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:]
    elif c.startswith('rgb('):
        # remove the rgb(...) stuff
        tmp = c.strip()[4:-1]
        numbers = [number.strip() for number in tmp.split(',')]
        converted_numbers = []
        if len(numbers) == 3:
            for num in numbers:
                if num.endswith(r'%'):
                    converted_numbers.append( int(float(num[0:-1])*255/100))
                else:
                    converted_numbers.append(int(num))
            return tuple(converted_numbers)
        else:    
            return (0,0,0)
    try:
        r=int(c[1:3],16)
        g=int(c[3:5],16)
        b=int(c[5:],16)
    except:
        return (0,0,0)
    return (r,g,b)

def parseStyle(s):
    """Create a dictionary from the value of an inline style attribute"""
    # This version strips leading and trailing whitespace from keys and values
    if s:
        return dict([map(string.strip,i.split(":")) for i in s.split(";") if len(i)])
    else:
        return {}

class GraphicsState(object):
    """A class for handling the graphics state of an SVG element
    
    The graphics state includs fill, stroke and transformations.
    """
    fill = {}
    stroke = {}
    is_visible = True
    transform = []
    color = None
    opacity = 1
    def __init__(self, svg_node):
        self.svg_node = svg_node
        self._parent_states = None
        self._get_graphics_state(svg_node)
        
    def _get_graphics_state(self, node):
        """Return the painting state of the node SVG element"""
        if node is None: return
        style = parseStyle(node.get('style',''))
        # get stroke and fill properties
        stroke = {}
        fill = {}
        
        for stroke_property in STROKE_PROPERTIES:
            stroke_val = style.get(stroke_property) or node.get(stroke_property)
            if stroke_val:
                stroke[stroke_property] = stroke_val
                
        for fill_property in FILL_PROPERTIES:
            fill_val = style.get(fill_property) or node.get(fill_property)
            if fill_val:
                fill[fill_property] = fill_val
        
        display = style.get('display') or node.get('display')
        visibility = style.get('visibility') or node.get('visibility')
        if display == 'none' or visibility == 'hidden':
            is_visible = False
        else:
            is_visible = True
        
        self.color = style.get('color') or node.get('color')
        self.stroke = stroke
        self.fill = fill
        self.is_visible = is_visible
        opacity = style.get('opacity') or node.get('opacity')
        if opacity:
            self.opacity = opacity
        else:
            self.opacity = 1
        
        transform = node.get('transform','')
        if transform:
            self.transform = parse_transform(transform)
        else:
            self.transform = []
        
    def _get_parent_states(self, node=None):
        """Returns the parent's graphics states as a list"""
        if node == None:
            node = self.svg_node
        parent_node = node.getparent()
        if not parent_node:
            return None
        parents_state = []
        while parent_node:
            parents_state.append(GraphicsState(parent_state))
            parent_node = parent_node.getparent()
        return parents_state
            
            
    parent_states = property(fget=_get_parent_states)
    
    def accumulate(self, state):
        newstate = GraphicsState(None)
        newstate.fill = copy.copy(self.fill)
        newstate.stroke = copy.copy(self.stroke)
        newstate.transform = copy.copy(self.transform)
        newstate.opacity = copy.copy(self.opacity)
        newstate.fill.update(state.fill)
        newstate.stroke.update(state.stroke)
        if newstate.stroke.get('stroke','') == 'none':
            del newstate.stroke['stroke']
        if newstate.fill.get('fill','') == 'none':
            del newstate.fill['fill']
        newstate.transform += state.transform
        newstate.is_visible = self.is_visible and state.is_visible
        if state.color:
            newstate.color = state.color
            
        newstate.opacity *= state.opacity
        return newstate
        
    def __str__(self):
        return "fill %s\nstroke: %s\nvisible: %s\ntransformations: %s" %\
        (self.fill, self.stroke, self.is_visible, self.transform)
    

class TikZPathExporter(inkex.Effect):
    def __init__(self, inkscape_mode=True):
        self.inkscape_mode = inkscape_mode
        inkex.Effect.__init__(self)
        
        parser = self.OptionParser
        parser.set_defaults(codeoutput='standalone', crop=False, clipboard=False,
                            wrap=True, indent=True, returnstring=False,
                            mode='effect', notext=False, verbose=False)
        parser.add_option('--codeoutput', dest='codeoutput',
                  choices = ('standalone','codeonly', 'figonly'),
                  help = "Amount of boilerplate code (standalone, figonly, codeonly).")

        self._add_booloption(parser, '--crop',
                        dest="crop", 
                        help="Use the preview package to crop the tikzpicture")
        self._add_booloption(parser, '--clipboard',
                        dest="clipboard",
                        help="Export to clipboard")
        self._add_booloption(parser, '--wrap',
                        dest="wrap",
                        help="Wrap long lines")
        self._add_booloption(parser, '--indent', default=True)
        parser.add_option("-o","--output",
                        action="store", type="string", 
                        dest="outputfile", default=None,
                        help="")
        if self.inkscape_mode:
            parser.add_option('--returnstring',action='store_true',dest='returnstring',
                              help="Return as string")
        
        parser.add_option('-m','--mode', dest='mode',
                  choices = ('output','effect', 'cli'),help="Extension mode (effect default)")
        
        self._add_booloption(parser, '--notext', dest='ignore_text',default=False, 
                          help="Ignore all text")
        if not self.inkscape_mode:
            parser.add_option('--standalone', dest='codeoutput',
                              action='store_const', const='standalone',
                              help="Generate a standalone document")
            parser.add_option('--figonly', dest='codeoutput',
                              action='store_const', const='figonly',
                              help="Generate figure only")
            parser.add_option('--codeonly', dest='codeoutput',
                              action='store_const', const='codeonly',
                              help="Generate drawing code only")
        self._add_booloption(parser, '--verbose', dest='verbose',default=False, 
                          help="Verbose output (useful for debugging)")    
        
        self.text_indent = ''
        self.x_o = self.y_o = 0.0
        # px -> cm scale factors
        self.x_scale = 0.02822219;
        # SVG has its origin in the upper left corner, while TikZ' origin is
        # in the lower left corner. We therefore have to reverse the y-axis.
        self.y_scale = -0.02822219;
        self.colors = {}
        self.colorcode = ""
        self.gradient_code = ""
        self.output_code = ""
        self.used_gradients = set()

    def parse(self, file_or_string=None):
        """Parse document in specified file or on stdin"""
        try:
            if file_or_string:
                try:
                    stream = open(file_or_string,'r')
                except:
                    stream = StringIO.StringIO(file_or_string)
            else:
                stream = open(self.args[-1],'r')
        except:
            stream = sys.stdin
        self.document = inkex.etree.parse(stream)
        stream.close()

    def _add_booloption(self, parser, *args, **kwargs):
        if self.inkscape_mode:
            kwargs['action'] = 'store'
            kwargs['type'] = 'inkbool'
            parser.add_option(*args, **kwargs)
        else:
            kwargs['action'] = 'store_true'
            parser.add_option(*args, **kwargs)
        
    def getselected(self):
        """Get selected nodes in document order

        The nodes are stored in the selected dictionary and as a list of
        nodes in selected_sorted.
        """
        self.selected_sorted = []
        self.selected = {}
        if len(self.options.ids) == 0:
            return
        # Iterate over every element in the document
        for node in self.document.getiterator():
            id = node.get('id','')
            if id in self.options.ids:
                self.selected[id] = node
                self.selected_sorted.append(node)

    def get_node_from_id(self,node_ref):
        if node_ref.startswith('url('):
            node_id = re.findall(r'url\((.*?)\)', node_ref)
            if len(node_id) > 0:
                ref_id = node_id[0]
        else:
            ref_id = node_ref
        if ref_id.startswith('#'):
            ref_id = ref_id[1:]
    
        ref_node = self.document.xpath('//*[@id="%s"]' % ref_id,
                                           namespaces=inkex.NSS)
        if len(ref_node) == 1:
            return ref_node[0]
        else:
            return None
        
            
    
    def transform(self, coord_list, cmd=None):
        """Apply transformations to input coordinates"""
        coord_transformed = []
        # TEMP:
        if cmd == 'Q':
            return tuple(coord_list)
        try:
            if not len(coord_list) % 2:
                for x, y in nsplit(coord_list,2):
                    #coord_transformed.append("%.4fcm" % ((x-self.x_o)*self.x_scale))
                    #oord_transformed.append("%.4fcm" % ((y-self.y_o)*self.y_scale))
                    coord_transformed.append("%.4f" % x)
                    coord_transformed.append("%.4f" % y)
            elif len(coord_list)==1:
                coord_transformed = ["%.4fcm" % (coord_list[0]*self.x_scale)]
            else:
                coord_transformed = coord_list
        except:
            coord_transformed = coord_list
        return tuple(coord_transformed)

    def get_color(self, color):
        """Return a valid xcolor color name and store color"""

        if color in self.colors:
            return self.colors[color]
        else:
            r,g,b = parseColor(color)
            if not (r or g or b):
                return "black"
            if color.startswith('rgb'):
                xcolorname = "c%02x%02x%02x" % (r,g,b)
            else:
                xcolorname = color.replace('#','c')
            self.colors[color] = xcolorname
            self.colorcode += "\\definecolor{%s}{RGB}{%s,%s,%s}\n" \
                              % (xcolorname,r,g,b)
            return xcolorname
    
    def _convert_gradient(self, gradient_node, gradient_tikzname):
        """Convert an SVG gradient to a PGF gradient"""
        # http://www.w3.org/TR/SVG/pservers.html
        def bpunit(offset):
            bp_unit = ""
            if offset.endswith("%"):
                bp_unit = offset[0:-1]
            else:
                bp_unit = str(int(round((float(offset))*100)));
                #bpunit = round()
            return bp_unit;
        if gradient_node.tag == inkex.addNS('linearGradient','svg'):
            c = "";
            c += "\pgfdeclarehorizontalshading{%s}{100bp}{\n" % gradient_tikzname
            stops = []
            for n in gradient_node:
                if n.tag == inkex.addNS('stop', 'svg'):
                    stops.append("color(%spt)=(%s)" % (bpunit(n.get("offset")), self.get_color(n.get("stop-color"))))
            c += ";".join(stops)
            c += "\n}\n"        
            return c
            
        else:
            return ""
    
    def _handle_gradient(self, gradient_ref, node=None):
        grad_node = self.get_node_from_id(gradient_ref)
        gradient_id = grad_node.get('id')
        if grad_node == None:
            return []
        gradient_tikzname = gradient_id
        if gradient_id not in self.used_gradients:
            grad_code = self._convert_gradient(grad_node, gradient_tikzname)
            if grad_code:
                self.gradient_code += grad_code
                self.used_gradients.add(gradient_id)
        if gradient_id in self.used_gradients:
            return ['shade', 'shading=%s' % gradient_tikzname]
        else:
            return []
        
    def convert_svgstate_to_tikz(self, state, accumulated_state=None, node=None):
        """Return a node's SVG styles as a list of TikZ options"""
        if state.is_visible == False:
            return [], []
            
        options = []
        transform = []
        
        if state.color:
            options.append('color=%s' % self.get_color(state.color))
        
        stroke = state.stroke.get('stroke','')
        if stroke <> 'none':
            if stroke:
                if stroke == 'currentColor':
                    options.append('draw')
                else:
                    options.append('draw=%s' % self.get_color(stroke))
            else:
                # need to check if parent element has stroke set
                if 'stroke' in accumulated_state.stroke:
                    options.append('draw')
                
        fill = state.fill.get('fill')
        if fill <> 'none':
            if fill:
                if fill == 'currentColor':
                    options.append('fill')
                elif fill.startswith('url('):
                    shadeoptions = self._handle_gradient(fill)
                    options.extend(shadeoptions)
                else:
                    options.append('fill=%s' % self.get_color(fill))
            else:
                # Todo: check parent element
                if 'fill' in accumulated_state.fill:
                    options.append('fill')
    
        # dash pattern has to come before dash phase. This is a bug in TikZ 2.0
        # Fixed in CVS.             
        dasharray = state.stroke.get('stroke-dasharray')
        if dasharray and dasharray <> 'none':
            lengths = map(inkex.unittouu, [i.strip() for i in dasharray.split(',')])
            dashes = []
            for idx, length in enumerate(lengths):
                lenstr = "%0.2fpt" % (length*0.8)
                if idx % 2 :
                    dashes.append("off %s" % lenstr)
                else:
                    dashes.append("on %s" % lenstr)
            options.append('dash pattern=%s' % " ".join(dashes))
        
        try:
            opacity = float(state.opacity)
            if opacity < 1.0:
                options.append('opacity=%.03f' % opacity)
        except:
            pass
        
        for svgname, tikzdata in PROPERTIES_MAP.iteritems():
            tikzname, valuetype,data = tikzdata
            value = state.fill.get(svgname) or state.stroke.get(svgname)
            if not value: continue
            if valuetype == SCALE:
                val = float(value)
                if not val == 1:
                    options.append('%s=%.3f' % (tikzname,float(value)))
            elif valuetype == DICT:
                if tikzname:
                    options.append('%s=%s' % (tikzname,data.get(value,'')))
                else:
                    options.append('%s' % data.get(value,''))
            elif valuetype == DIMENSION:
                # FIXME: Handle different dimensions in a general way
                if value and value <> data:
                    options.append('%s=%.3fpt' % (tikzname,inkex.unittouu(value)*0.80)),
            elif valuetype == FACTOR:
                try:
                    val = float(value)
                    if val >= 1.0:
                        options.append('%s=%.2f' % (tikzname,val))
                except ValueError:
                    pass
            
        if len(state.transform) > 0:
            transform = self._convert_transform_to_tikz(state.transform)
        else:
            transform = []
               
        return options, transform
    
    
    def _convert_transform_to_tikz(self, transform):
        """Convert a SVG transform attribute to a list of TikZ transformations"""
        #return ""
        if not transform:
            return []

        options = []
        for cmd, params in transform:
            if cmd == 'translate':
                x, y = params
                options.append("shift={(%s,%s)}" % (x or '0',y or '0'))
                # There is bug somewere.
                # shift=(400,0) is not equal to xshift=400
                
            elif cmd == 'rotate':
                if params[1] or params[2]:
                    options.append("rotate around={%s:(%s,%s)}" % params)
                else:
                    options.append("rotate=%s" % params[0])
            elif cmd == 'matrix':
                options.append("cm={{%s,%s,%s,%s,(%s,%s)}}" % params)
            elif cmd == 'skewX':
                options.append("xslant=%.3f" % math.tan(params[0]*math.pi/180))
            elif cmd == 'skewY':
                options.append("yslant=%.3f" % math.tan(params[0]*math.pi/180))
            elif cmd == 'scale':
                if params[0]==params[1]:
                    options.append("scale=%.3f" % params[0])
                else:
                    options.append("xscale=%.3f,yscale=%.3f" % params)

        return options

    def _handle_group(self, groupnode, graphics_state, accumulated_state):
        s = ""
        tmp = self.text_indent
        
        self.text_indent += TEXT_INDENT
        id = groupnode.get('id')
        code = self._output_group(groupnode, accumulated_state.accumulate(graphics_state))
        self.text_indent = tmp
        if self.options.verbose and id:
            extra = "%% %s" % id
        else:
            extra = ''
        goptions, transformation = self.convert_svgstate_to_tikz(graphics_state, graphics_state, groupnode)
        options = transformation + goptions
        if len(options) > 0:
            pstyles = [','.join(options)]
            if 'opacity' in pstyles[0]:
                pstyles.append('transparency group')
            
            if self.options.indent:                        
                s += "%s\\begin{scope}[%s]%s\n%s%s\\end{scope}\n" % \
                    (self.text_indent,",".join(pstyles), extra,
                     code,self.text_indent)
            else:
                s += "\\begin{scope}[%s]%s\n%s\\end{scope}\n" % \
                    (",".join(pstyles), extra, code)
        elif self.options.verbose:
            if self.options.indent:                        
                s += "%s\\begin{scope}%s\n%s%s\\end{scope}\n" % \
                    (self.text_indent, extra, code, self.text_indent)
            else:
                s += "\\begin{scope}\n%s\\end{scope}\n" % \
                    (code,)
        else:
            s += code
        return s

    def _handle_image(self, node):
        """Handles the image tag and returns a code, options tuple"""
        # http://www.w3.org/TR/SVG/struct.html#ImageElement
        # http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
        x = node.get('x','0')
        y = node.get('y','0')
        print "%% Href %s" % node.get(inkex.addNS('href','xlink'))
        return None, []
    
    def _handle_path(self, node):
        p = simplepath.parsePath(node.get('d'))
        return p, []
    
    def _handle_shape(self, node):
        """Extract shape data from node"""
        options = []
        if node.tag == inkex.addNS('rect','svg'):
            inset = node.get('rx','0') or node.get('ry','0')
            # TODO: ry <> rx is not supported by TikZ. Convert to path?
            x = inkex.unittouu(node.get('x','0'))
            y = inkex.unittouu(node.get('y','0'))
            # map from svg to tikz
            width = inkex.unittouu(node.get('width','0'))
            height = inkex.unittouu(node.get('height','0'))
            if (width == 0.0 or height == 0.0):
                return None, []
            if inset:
                # TODO: corner radius is not scaled by PGF. Find a better way to fix this. 
                options = ["rounded corners=%s" % self.transform([inkex.unittouu(inset)*0.8])]
            return ('rect',(x,y,width+x,height+y)),options
        elif node.tag in [inkex.addNS('polyline','svg'),
                          inkex.addNS('polygon','svg'),
                          ]:
            points = node.get('points','').replace(',',' ')
            points = map(inkex.unittouu,points.split())
            if node.tag == inkex.addNS('polyline','svg'):
                cmd = 'polyline'
            else:
                cmd = 'polygon'
    
            return (cmd,points),options
        elif node.tag in inkex.addNS('line','svg'):
            points = [node.get('x1'),node.get('y1'),
                      node.get('x2'),node.get('y2')]
            points = map(inkex.unittouu,points)
            # check for zero lenght line
            if not ((points[0] == points[2]) and (points[1] == points[3])):
                return ('polyline',points),options

        if node.tag == inkex.addNS('circle','svg'):
            # ugly code...
            center = map(inkex.unittouu,[node.get('cx','0'),node.get('cy','0')])
            r = inkex.unittouu(node.get('r','0'))
            if r > 0.0:
                return ('circle',self.transform(center)+self.transform([r])),options

        elif node.tag == inkex.addNS('ellipse','svg'):
            center = map(inkex.unittouu,[node.get('cx','0'),node.get('cy','0')])
            rx = inkex.unittouu(node.get('rx','0'))
            ry = inkex.unittouu(node.get('ry','0'))
            if rx > 0.0 and ry > 0.0:
                return ('ellipse',self.transform(center)+self.transform([rx])
                                 +self.transform([ry])),options
        else:
            return None,options
        
        return None, options
            
    def _handle_text(self, node):
        if not self.options.ignore_text:
            textstr = self.get_text(node)
            x = node.get('x','0')
            y = node.get('y','0')
            p = [('M',[x,y]),('TXT',textstr)]
            return p, []
        else:
            return None, []
    
    def _handle_use(self, node, graphics_state, accumulated_state=None):
        # Find the id of the use element link
        ref_id = node.get(inkex.addNS('href', 'xlink'))
        if ref_id.startswith('#'):
            ref_id = ref_id[1:]
    
        use_ref_node = self.document.xpath('//*[@id="%s"]' % ref_id,
                                           namespaces=inkex.NSS)
        if len(use_ref_node) == 1:
            use_ref_node = use_ref_node[0]
        else:
            return ""
        
        # create a temp group
        g_wrapper = inkex.etree.Element(inkex.addNS('g', 'svg'))
        use_g = inkex.etree.SubElement(g_wrapper,inkex.addNS('g', 'svg'))
        
        # transfer attributes from use element to new group except
        # x, y, width, height and href
        for key in node.keys():
            if key not in ('x', 'y', 'width', 'height',
                           inkex.addNS('href', 'xlink')):
                use_g.set(key, node.get(key))
        if node.get('x') or node.get('y'):
            transform = node.get('transform','')
            transform += ' translate(%s,%s) '\
                % (node.get('x',0), node.get('y',0))
            use_g.set('transform', transform)       
        #
        use_g.append( deepcopy(use_ref_node) )
        return self._output_group(g_wrapper, accumulated_state)
    
    def _write_tikz_path(self, pathdata, options=[], node=None):
        """Convert SVG paths, shapes and text to TikZ paths"""
        s = pathcode = ""
        #print "Pathdata %s" % pathdata
        if not pathdata or len(pathdata) == 0:
            return ""
        if node is not None:
            id = node.get('id', '')
        else:
            id = ''
        
        current_pos = [0.0,0.0]
        for cmd,params in pathdata:
            # transform coordinates
            tparams = self.transform(params,cmd)
            # SVG paths
            # moveto
            if cmd == 'M':
                s += "(%s,%s)" % tparams
                current_pos = params[-2:]
            # lineto
            elif cmd == 'L':
                s += " -- (%s,%s)" % tparams
                current_pos = params[-2:]
            # cubic bezier curve
            elif cmd == 'C':
                s += " .. controls (%s,%s) and (%s,%s) .. (%s,%s)" % tparams
                current_pos = params[-2:]
            # quadratic bezier curve 
            elif cmd == 'Q':
                # need to convert to cubic spline
                #CP1 = QP0 + 2/3 *(QP1-QP0)
                #CP2 = CP1 + 1/3 *(QP2-QP0)
                # http://fontforge.sourceforge.net/bezier.html
                qp0x, qp0y = current_pos
                qp1x,qp1y,qp2x,qp2y = tparams
                cp1x = qp0x +(2.0/3.0)*(qp1x-qp0x)
                cp1y = qp0y +(2.0/3.0)*(qp1y-qp0y)
                cp2x = cp1x +(qp2x-qp0x)/3.0
                cp2y = cp1y +(qp2y-qp0y)/3.0
                s += " .. controls (%.4f,%.4f) and (%.4f,%.4f) .. (%.4f,%.4f)"\
                     % (cp1x,cp1y,cp2x,cp2y,qp2x,qp2y)
                current_pos = params[-2:]
            # close path
            elif cmd == 'Z':
                s += " -- cycle"
                closed_path = True
            # arc
            elif cmd == 'A':
                start_ang_o, end_ang_o, rx, ry = calc_arc(current_pos[0],current_pos[1],*params)
                # pgf 2.0 does not like angles larger than 360
                # make sure it is in the +- 360 range
                start_ang = start_ang_o % 360
                end_ang = end_ang_o % 360
                if start_ang_o < end_ang_o and not (start_ang < end_ang):
                    start_ang -= 360
                elif start_ang_o > end_ang_o and not (start_ang > end_ang):
                    end_ang -= 360  
                ang = params[2]
                if rx == ry:
                    # Todo: Transform radi
                    radi = "%.3f" % rx
                else:
                    radi = "%3f and %.3f" % (rx,ry)
                if ang <> 0.0:
                    s += "{[rotate=%s] arc(%.3f:%.3f:%s)}" % (ang,start_ang,end_ang,radi)
                else:
                    s += "arc(%.3f:%.3f:%s)" % (start_ang,end_ang,radi)
                current_pos = params[-2:]
                pass
            elif cmd == 'TXT':
                s += " node[above right] (%s) {%s}" %(id,params)
            # Shapes
            elif cmd == 'rect':
                s += "(%s,%s) rectangle (%s,%s)" % tparams
                closed_path = True
            elif cmd in ['polyline','polygon']:
                points = ["(%s,%s)" % (x,y) for x,y in chunks(tparams,2)]
                if cmd == 'polygon':
                    points.append('cycle')
                    closed_path = True
                s += " -- ".join(points)
            # circle and ellipse does not use the transformed parameters
            elif cmd == 'circle':
                s += "(%s,%s) circle (%s)" % params
                closed_path = True
            elif cmd == 'ellipse':
                s += "(%s,%s) ellipse (%s and %s)" % params
                closed_path = True
        
        if options:
            optionscode = "[%s]" % ','.join(options)
        else:
            optionscode = ""
            
        pathcode = "\\path%s %s;" % (optionscode,s)
        if self.options.wrap:
            pathcode = "\n".join(wrap(pathcode, 80, subsequent_indent="  ",
                                      break_long_words=False))
    
        if self.options.indent:
            pathcode = "\n".join([self.text_indent + line for line in pathcode.splitlines(False)])+"\n"
        if self.options.verbose and id:
            pathcode = "%s%% %s\n%s\n" % (self.text_indent, id, pathcode)
        return pathcode
    
    def get_text(self, node):
        """Return content of a text node as string"""
        # For recent versions of lxml we can simply write:
        # return etree.tostring(node,method="text")
        text = ""
        if node.text != None:
            text += node.text
        for child in node:
            text += self.get_text(child)
        if node.tail:
            text += node.tail
        return text

    def _output_group(self, group, accumulated_state=None):
        """Proceess a group of SVG nodes and return corresponding TikZ code
        
        The group is processed recursively if it contains sub groups. 
        """
        s = ""
        options = []
        transform = []
        for node in group:
            pathdata = None
            options = []
            graphics_state = GraphicsState(node)
            #print graphics_state 
            id = node.get('id')
            if node.tag == inkex.addNS('path','svg'):
                pathdata, options = self._handle_path(node)
                
                
            # is it a shape?
            elif node.tag in [inkex.addNS('rect','svg'),
                              inkex.addNS('polyline','svg'),
                              inkex.addNS('polygon','svg'),
                              inkex.addNS('line','svg'),
                              inkex.addNS('circle','svg'),
                              inkex.addNS('ellipse','svg'),]:
                shapedata, options = self._handle_shape(node)
                if shapedata:
                    pathdata = [shapedata]
            elif node.tag == inkex.addNS('image', 'svg'):
                pathdata, options = self._handle_image(node)
                
            # group node
            elif node.tag == inkex.addNS('g', 'svg'):
                s += self._handle_group(node, graphics_state, accumulated_state)
                continue
                
            elif node.tag == inkex.addNS('text', 'svg'):
                pathdata, options = self._handle_text(node)
                
            elif node.tag == inkex.addNS('use', 'svg'):
                s += self._handle_use(node, graphics_state, accumulated_state)

            else:
                # unknown element
                pass
            
            goptions, transformation = self.convert_svgstate_to_tikz(graphics_state, accumulated_state, node)
            #print goptions, transformation, options
            options = transformation + goptions + options
            s += self._write_tikz_path(pathdata, options, node)
        return s


    def effect(self):
        s = ""
        nodes = self.selected_sorted
        # If no nodes is selected convert whole document. 
        if len(nodes) == 0:
            nodes = self.document.getroot()
            graphics_state = GraphicsState(nodes)
        else:
            graphics_state = GraphicsState(None)
        goptions, transformation = self.convert_svgstate_to_tikz(graphics_state, graphics_state, self.document.getroot())
        options = transformation + goptions
        # Recursively process list of nodes or root node
        s = self._output_group(nodes, graphics_state)

        # Add necessary boiling plate code to the generated TikZ code. 
        codeoutput = self.options.codeoutput
        if len(options) > 0:
            extraoptions = ',\n%s' % ','.join(options)
        else:
            extraoptions = ''
        if not self.options.crop:
            cropcode = ""
        else:
            cropcode = CROP_TEMPLATE
        if codeoutput == 'standalone':
            output = STANDALONE_TEMPLATE % dict(pathcode=s,\
                                                colorcode=self.colorcode,\
                                                cropcode=cropcode,\
                                                extraoptions=extraoptions,\
                                                gradientcode=self.gradient_code)
        elif codeoutput == 'figonly':
            output = FIG_TEMPLATE % dict(pathcode=s, colorcode=self.colorcode,\
                                         extraoptions=extraoptions,\
                                         gradientcode=self.gradient_code)
        else:
            output = s
        
        self.output_code = output    
        if self.options.returnstring:
            return output
                
    def output(self):
        if self.options.clipboard:
            success = copy_to_clipboard(self.output_code)
            if not success:
                logging.error('Failed to put output on clipboard')
        if self.options.mode == 'effect':
            if self.options.outputfile and not self.options.clipboard:
                f = codecs.open(self.options.outputfile, 'w', 'utf8')
                f.write(self.output_code)
                f.close()
            # Serialize document into XML on stdout
            self.document.write(sys.stdout) 
            
        if self.options.mode == 'output':
            print self.output_code.encode('utf8')
            
    def convert(self,svg_file,**kwargs):
        self.getoptions()
        self.options.returnstring = True
        #self.options.crop=True
        self.options.__dict__.update(kwargs)
        self.parse(svg_file)
        self.getposinlayer()
        self.getselected()
        self.getdocids()
        output = self.effect()
        if self.options.clipboard:
            success = copy_to_clipboard(self.output_code)
            if not success:
                logging.error('Failed to put output on clipboard')
            output = ""
        
        if self.options.outputfile:
            f = codecs.open(self.options.outputfile, 'w', 'utf8')
            f.write(self.output_code)
            f.close()
            output = ""
            
        return output
        
def convert_file(svg_file, **kwargs):
    effect = TikZPathExporter(inkscape_mode=False);
    return effect.convert(svg_file,**kwargs)
    
def convert_svg(svg_source, **kwargs):
    effect = TikZPathExporter(inkscape_mode=False);
    source = open_anything(svg_source)
    tikz_code = effect.convert(source.read(),**kwargs)
    source.close()
    return tikz_code 
    
def main_inkscape():
    """Inkscape interface"""
    # Create effect instance and apply it.
    effect = TikZPathExporter(inkscape_mode=True)
    effect.affect()
    
def main_cmdline(**kwargs):
    """Main command line interface"""
    effect = TikZPathExporter(inkscape_mode=False);
    tikz_code = effect.convert(svg_file=None, **kwargs)
    print tikz_code.encode('utf8')


if __name__ == '__main__':
    main_inkscape()
